解决 TS 问题的最好办法就是多练,这次解读 [type-challenges](https://github.com/type-challenges/type-challenges) Medium 难度 41~48 题。 ## 精读 ### [ObjectEntries](https://github.com/type-challenges/type-challenges/blob/main/questions/02946-medium-objectentries/README.md) 实现 TS 版本的 `Object.entries`: ```ts interface Model { name: string; age: number; locations: string[] | null; } type modelEntries = ObjectEntries // ['name', string] | ['age', number] | ['locations', string[] | null]; ``` 经过前面的铺垫,大家应该熟悉了 TS 思维思考问题,这道题看到后第一个念头应该是:如何先把对象转换为联合类型?这个问题不解决,就无从下手。 对象或数组转联合类型的思路都是类似的,一个数组转联合类型用 `[number]` 作为下标: ```ts ['1', '2', '3']['number'] // '1' | '2' | '3' ``` 对象的方式则是 `[keyof T]` 作为下标: ```ts type ObjectToUnion = T[keyof T] ``` 再观察这道题,联合类型每一项都是数组,分别是 Key 与 Value,这样就比较好写了,我们只要构造一个 Value 是符合结构的对象即可: ```ts type ObjectEntries = { [K in keyof T]: [K, T[K]] }[keyof T] ``` 为了通过单测 `ObjectEntries<{ key?: undefined }>`,让 Key 位置不出现 `undefined`,需要强制把对象描述为非可选 Key: ```TS type ObjectEntries = { [K in keyof T]-?: [K, T[K]] }[keyof T] ``` 为了通过单测 `ObjectEntries>`,得将 Value 中 `undefined` 移除: ```ts // 本题答案 type RemoveUndefined = [T] extends [undefined] ? T : Exclude type ObjectEntries = { [K in keyof T]-?: [K, RemoveUndefined] }[keyof T] ``` ### [Shift](https://github.com/type-challenges/type-challenges/blob/main/questions/03062-medium-shift/README.md) 实现 TS 版 `Array.shift`: ```ts type Result = Shift<[3, 2, 1]> // [2, 1] ``` 这道题应该是简单难度的,只要把第一项抛弃即可,利用 `infer` 轻松实现: ```ts // 本题答案 type Shift = T extends [infer First, ...infer Rest] ? Rest : never ``` ### [Tuple to Nested Object](https://github.com/type-challenges/type-challenges/blob/main/questions/03188-medium-tuple-to-nested-object/README.md) 实现 `TupleToNestedObject`,其中 `T` 仅接收字符串数组,`P` 是任意类型,生成一个递归对象结构,满足如下结果: ```ts type a = TupleToNestedObject<['a'], string> // {a: string} type b = TupleToNestedObject<['a', 'b'], number> // {a: {b: number}} type c = TupleToNestedObject<[], boolean> // boolean. if the tuple is empty, just return the U type ``` 这道题用到了 5 个知识点:递归、辅助类型、`infer`、如何指定对象 Key、`PropertyKey`,你得全部知道并组合起来才能解决该题。 首先因为返回值是个递归对象,递归过程中必定不断修改它,因此给泛型添加第三个参数 `R` 存储这个对象,并且在递归数组时从最后一个开始,这样从最内层对象开始一点点把它 “包起来”: ```ts type TupleToNestedObject = /** 伪代码 T extends [...infer Rest, infer Last] */ ``` 下一步是如何描述一个对象 Key?之前 `Chainable Options` 例子我们学到的 `K in Q`,但需要注意直接这么写会报错,因为必须申明 `Q extends PropertyKey`。最后再处理一下递归结束条件,即 `T` 变成空数组时直接返回 `R`: ```ts // 本题答案 type TupleToNestedObject = T extends [] ? R : ( T extends [...infer Rest, infer Last extends PropertyKey] ? ( TupleToNestedObject ) : never ) ``` ### [Reverse](https://github.com/type-challenges/type-challenges/blob/main/questions/03192-medium-reverse/README.md) 实现 TS 版 `Array.reverse`: ```ts type a = Reverse<['a', 'b']> // ['b', 'a'] type b = Reverse<['a', 'b', 'c']> // ['c', 'b', 'a'] ``` 这道题比上一题简单,只需要用一个递归即可: ```ts // 本题答案 type Reverse = T extends [...infer Rest, infer End] ? [End, ...Reverse] : T ``` ### [Flip Arguments](https://github.com/type-challenges/type-challenges/blob/main/questions/03196-medium-flip-arguments/README.md) 实现 `FlipArguments` 将函数 `T` 的参数反转: ```ts type Flipped = FlipArguments<(arg0: string, arg1: number, arg2: boolean) => void> // (arg0: boolean, arg1: number, arg2: string) => void ``` 本题与上题类似,只是反转内容从数组变成了函数的参数,只要用 `infer` 定义出函数的参数,利用 `Reverse` 函数反转一下即可: ```ts // 本题答案 type Reverse = T extends [...infer Rest, infer End] ? [End, ...Reverse] : T type FlipArguments = T extends (...args: infer Args) => infer Result ? (...args: Reverse) => Result : never ``` ### [FlattenDepth](https://github.com/type-challenges/type-challenges/blob/main/questions/03243-medium-flattendepth/README.md) 实现指定深度的 Flatten: ```ts type a = FlattenDepth<[1, 2, [3, 4], [[[5]]]], 2> // [1, 2, 3, 4, [5]]. flattern 2 times type b = FlattenDepth<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, [[5]]]. Depth defaults to be 1 ``` 这道题比之前的 `Flatten` 更棘手一些,因为需要控制打平的次数。 基本想法就是,打平 `Deep` 次,所以需要实现打平一次的函数,再根据 `Deep` 值递归对应次: ```ts type FlattenOnce = T extends [infer X, ...infer Y] ? ( X extends any[] ? FlattenOnce : FlattenOnce ) : U ``` 然后再实现主函数 `FlattenDepth`,因为 TS 无法实现 +、- 号运算,我们必须用数组长度判断与操作数组来辅助实现: ```ts // FlattenOnce type FlattenDepth< T extends any[], U extends number = 1, P extends any[] = [] > = P['length'] extends U ? T : ( FlattenDepth, U, [...P, any]> ) ``` 当递归没有达到深度 `U` 时,就用 `[...P, any]` 的方式给数组塞一个元素,下次如果能匹配上 `P['length'] extends U` 说明递归深度已达到。 但考虑到测试用例 `FlattenDepth<[1, [2, [3, [4, [5]]]]], 19260817>` 会引发超长次数递归,需要提前终止,即如果打平后已经是平的,就不用再继续递归了,此时可以用 `FlattenOnce extends T` 判断: ```ts // 本题答案 // FlattenOnce type FlattenDepth< T extends any[], U extends number = 1, P extends any[] = [] > = P['length'] extends U ? T : ( FlattenOnce extends T ? T : ( FlattenDepth, U, [...P, any]> ) ) ``` ### [BEM style string](https://github.com/type-challenges/type-challenges/blob/main/questions/03326-medium-bem-style-string/README.md) 实现 `BEM` 函数完成其规则拼接: ```ts Expect, 'btn--small' | 'btn--medium' | 'btn--large' >>, ``` 之前我们了解了通过下标将数组或对象转成联合类型,这里还有一个特殊情况,即字符串中通过这种方式申明每一项,会自动笛卡尔积为新的联合类型: ```ts type BEM = `${B}__${E[number]}--${M[number]}` ``` 这是最简单的写法,但没有考虑项不存在的情况。不如创建一个 `SafeUnion` 函数,当传入值不存在时返回空字符串,保证安全的跳过: ```ts type IsNever = TValue[] extends never[] ? true : false; type SafeUnion = IsNever extends true ? "" : TUnion; ``` 最终代码: ```ts // 本题答案 // IsNever, SafeUnion type BEM = `${B}${SafeUnion<`__${E[number]}`>}${SafeUnion<`--${M[number]}`>}` ``` ### [InorderTraversal](https://github.com/type-challenges/type-challenges/blob/main/questions/03376-medium-inordertraversal/README.md) 实现 TS 版二叉树中序遍历: ```ts const tree1 = { val: 1, left: null, right: { val: 2, left: { val: 3, left: null, right: null, }, right: null, }, } as const type A = InorderTraversal // [1, 3, 2] ``` 首先回忆一下二叉树中序遍历 JS 版的实现: ```js function inorderTraversal(tree) { if (!tree) return [] return [ ...inorderTraversal(tree.left), res.push(val), ...inorderTraversal(tree.right) ] } ``` 对 TS 来说,实现递归的方式有一点点不同,即通过 `extends TreeNode` 来判定它不是 Null 从而递归: ```ts // 本题答案 interface TreeNode { val: number left: TreeNode | null right: TreeNode | null } type InorderTraversal = [T] extends [TreeNode] ? ( [ ...InorderTraversal, T['val'], ...InorderTraversal ] ): [] ``` 你可能会问,问什么不能像 JS 一样,用 `null` 做判断呢? ```ts type InorderTraversal = [T] extends [null] ? [] : ( [ // error ...InorderTraversal, T['val'], ...InorderTraversal ] ) ``` 如果这么写会发现 TS 抛出了异常,因为 TS 不能确定 `T` 此时符合 `TreeNode` 类型,所以要执行操作时一般采用正向判断。 ## 总结 这些类型挑战题目需要灵活组合 TS 的基础知识点才能破解,常用的包括: - 如何操作对象,增减 Key、只读、合并为一个对象等。 - 递归,以及辅助类型。 - `infer` 知识点。 - 联合类型,如何从对象或数组生成联合类型,字符串模板与联合类型的关系。 > 讨论地址是:[精读《ObjectEntries, Shift, Reverse...》· Issue #431 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/431) **如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** > 关注 **前端精读微信公众号** > 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))